/*
 * Copyright 2002-2010 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

using System.Collections;
using System.Data;
using Microsoft.Extensions.Logging;
using Spring.Dao;
using Spring.Data.Common;
using Spring.Objects.Factory;

namespace Spring.Data.Objects;

/// <summary>
/// Abstract base class providing common functionality for generic and non
/// generic implementations of "AdoOperation" subclasses.
/// </summary>
/// <author>Mark Pollack (.NET)</author>
public abstract class AbstractAdoOperation : IInitializingObject
{
    protected readonly ILogger log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

    private string sql;

    private CommandType commandType = CommandType.Text;

    private IDbParameters declaredParameters;

    /// <summary>
    /// Has this operation been compiled?  Compilation means at
    /// least checking that a IDbProvider and sql have been provided,
    /// but subclasses may also implement their own custom validation.
    /// </summary>
    private bool compiled;

    /// <summary>
    /// Object enabling us to create IDbCommands
    /// efficiently, based on this class's declared parameters.
    /// </summary>
    private IDbCommandCreatorFactory commandFactory;

    /// <summary>
    /// Gets or sets the type of the command.
    /// </summary>
    /// <value>The type of the command.</value>
    public CommandType CommandType
    {
        get { return commandType; }
        set { commandType = value; }
    }

    /// <summary>
    /// Gets or sets the db provider.
    /// </summary>
    /// <value>The db provider.</value>
    public abstract IDbProvider DbProvider
    {
        get;
        set;
    }

    /// <summary>
    /// Gets or sets the SQL to execute
    /// </summary>
    /// <value>The SQL.</value>
    public string Sql
    {
        get
        {
            return sql;
        }
        set
        {
            sql = value;
        }
    }

    /// <summary>
    /// Gets or sets the declared parameters.
    /// </summary>
    /// <value>The declared parameters.</value>
    public IDbParameters DeclaredParameters
    {
        get
        {
            return declaredParameters;
        }
        set
        {
            if (Compiled)
            {
                throw new InvalidDataAccessApiUsageException("Cannot add parameters once operation is compiled");
            }

            declaredParameters = value;
        }
    }

    /// <summary>
    /// Gets a value indicating whether this <see cref="AdoOperation"/> is compiled.
    /// </summary>
    /// <remarks>Compilation means that the operation is fully configured,
    ///  and ready to use.  The exact meaning of compilation will vary between subclasses.</remarks>
    /// <value><c>true</c> if compiled; otherwise, <c>false</c>.</value>
    public bool Compiled
    {
        get { return compiled; }
        protected set { compiled = value; }
    }

    /// <summary>
    /// Sets the command timeout for IDbCommands that this AdoTemplate executes.
    /// </summary>
    /// <remarks>Default is 0, indicating to use the database provider's default.
    /// Any timeout specified here will be overridden by the remaining
    /// transaction timeout when executing within a transaction that has a
    /// timeout specified at the transaction level.
    /// </remarks>
    /// <value>The command timeout.</value>
    public abstract int CommandTimeout
    {
        set;
    }

    /// <summary>
    /// Ensures compilation if used in an IApplicationContext
    /// </summary>
    public void AfterPropertiesSet()
    {
        Compile();
    }

    /// <summary>
    /// Compiles this operation.  Ignores subsequent attempts to compile.
    /// </summary>
    public abstract void Compile();

    /// <summary>
    /// Check whether this operation has been compiled already;
    /// lazily compile it if not already compiled.
    /// </summary>
    /// <remarks>Automatically called by ValidateParameters and ValidateNamedParameters</remarks>
    protected void CheckCompiled()
    {
        if (!Compiled)
        {
            log.LogDebug("ADO operation not compiled before execution - invoking compile");
            Compile();
        }
    }

    protected virtual void ValidateParameters(params object[] inParamValues)
    {
        CheckCompiled();
        int declaredInParameters = 0;
        if (DeclaredParameters != null)
        {
            for (int i = 0; i < declaredParameters.Count; i++)
            {
                IDataParameter declaredParameter = DeclaredParameters[i];
                if (IsInputParameter(declaredParameter))
                {
                    declaredInParameters++;
                }
            }
        }

        if (inParamValues != null)
        {
            if (DeclaredParameters == null)
            {
                throw new InvalidDataAccessApiUsageException(
                    "Didn't expect any parameters: none were declared");
            }

            if (inParamValues.Length < declaredInParameters)
            {
                throw new InvalidDataAccessApiUsageException(
                    inParamValues.Length + " parameters were supplied, but " +
                    declaredInParameters + " in parameters were declared in class [" +
                    GetType().AssemblyQualifiedName + "]");
            }

            if (inParamValues.Length > declaredInParameters)
            {
                throw new InvalidDataAccessApiUsageException(
                    inParamValues.Length + " parameters were supplied, but " +
                    DeclaredParameters.Count + " parameters were declared " +
                    "in class [" + GetType().AssemblyQualifiedName + "]");
            }
        }
        else
        {
            // No parameters were supplied
            if (DeclaredParameters != null && !(DeclaredParameters.Count == 0))
            {
                throw new InvalidDataAccessApiUsageException(
                    DeclaredParameters.Count + " parameters must be supplied");
            }
        }
    }

    protected virtual bool IsInputParameter(IDataParameter parameter)
    {
        return (parameter.Direction == ParameterDirection.Input
                || parameter.Direction == ParameterDirection.InputOutput);
    }

    /// <summary>
    /// Validates the named parameters passed to an AdoTemplate ExecuteXXX method based on
    /// declared parameters.
    /// </summary>
    /// <remarks>
    /// Subclasses should invoke this method very every ExecuteXXX method.
    /// </remarks>
    /// <param name="parameters">The parameter dictionary supplied.  May by null.</param>
    protected virtual void ValidateNamedParameters(IDictionary parameters)
    {
        CheckCompiled();
        IDictionary paramsToUse = (parameters != null ? parameters : new Hashtable());

        if (declaredParameters != null)
        {
            for (int i = 0; i < declaredParameters.Count; i++)
            {
                IDataParameter declaredParameter = DeclaredParameters[i];
                if (declaredParameter.ParameterName == null)
                {
                    throw new InvalidDataAccessApiUsageException(
                        "All parameters must have name specified when using the methods " +
                        "dedicated to named parameter support");
                }

                if (IsInputParameter(declaredParameter) && !paramsToUse.Contains(declaredParameter.ParameterName))
                {
                    throw new InvalidDataAccessApiUsageException(
                        "The parameter named '" + declaredParameter +
                        "' was not among the parameters supplied: " + paramsToUse.Keys);
                }
            }
        }

        if (DeclaredParameters != null && DeclaredParameters.Count > 0)
        {
            if (DeclaredParameters == null)
            {
                throw new InvalidDataAccessApiUsageException(
                    "Didn't expect any parameters: none were declared");
            }
        }
        else
        {
            // No parameters were supplied
            if (DeclaredParameters != null && !(DeclaredParameters.Count == 0))
            {
                throw new InvalidDataAccessApiUsageException(
                    "Parameters must be supplied");
            }
        }
    }

    /// <summary>
    /// Subclasses must implement to perform their own compilation.
    /// Invoked after this class's compilation is complete.
    /// Subclasses can assume that SQL has been supplied and that
    /// a IDbProvider has been supplied.
    /// </summary>
    protected virtual void CompileInternal()
    {
        commandFactory = new IDbCommandCreatorFactory(DbProvider, CommandType, Sql, DeclaredParameters);
        OnCompileInternal();
    }

    /// <summary>
    /// Hook method that subclasses may override to react to compilation.
    /// This implementation does nothing.
    /// </summary>
    protected virtual void OnCompileInternal()
    {
    }

    protected virtual IDbCommandCreator NewCommandCreator(IDictionary inParams)
    {
        return commandFactory.NewDbCommandCreator(inParams);
    }

    protected virtual IDbCommandCreator NewCommandCreatorWithParamValues(params object[] inParams)
    {
        return commandFactory.NewDbCommandCreatorWithParamValues(inParams);
    }
}
